using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using Granite.Collections;
using Granite.Eventing;

namespace Rifts
{
	/// <summary>
	/// This creates a collection that represents a filtered view into another collection
	/// </summary>
	/// <typeparam name="T">Type of item in collection</typeparam>
	public class FilteredCollection<T> : ICollection<T>, INotifyCollectionChanged where T : INotifyPropertyChanged
	{
		readonly Predicate<T> m_FilterCriteria;
		readonly ExtendedObservableCollection<T> m_Source;
		readonly List<ItemStatus> m_KnownItems = new List<ItemStatus>();
		readonly List<T> m_VisibleItems = new List<T>();
		readonly Listener<NotifyCollectionChangedEventArgs> m_SourceCollectionChanged;
		readonly Listener<RelayedEventArgs<PropertyChangedEventArgs>> m_ItemPropertyChanged;
		readonly int? m_MinItems;
		readonly int? m_MaxItems;
		readonly Func<T> m_PaddingFunction;

		/// <summary>
		/// This creates a collection that represents a filtered view into another collection
		/// </summary>
		/// <param name="source">Source collection to observe</param>
		/// <param name="filterCriteria">Only items that evaluate as true will appear in the collection</param>
		/// <param name="minItems">This is the minimum number of items to show. The paddingFunction will be used to pad the list.</param>
		/// <param name="maxItems">This is the maximum number of items to show.</param>
		/// <param name="paddingFunction">This is the function used to pad the list when the count is less than MinItems. </param>
		public FilteredCollection(ExtendedObservableCollection<T> source, Predicate<T> filterCriteria, int? minItems = null, int? maxItems = null, Func<T> paddingFunction = null)
		{
			if (paddingFunction != null)
				m_PaddingFunction = paddingFunction;
			else
				m_PaddingFunction = () => default(T);

			m_MaxItems = maxItems;
			m_MinItems = minItems;
			m_FilterCriteria = filterCriteria;
			m_Source = source;
			m_SourceCollectionChanged = new Listener<NotifyCollectionChangedEventArgs>(SourceCollectionChanged);
			m_ItemPropertyChanged = new Listener<RelayedEventArgs<PropertyChangedEventArgs>>(OnItemPropertyChanged);
			m_Source.AddHandler(m_SourceCollectionChanged);
			m_Source.AddHandler(m_ItemPropertyChanged);
			UpdateVisibleList();
		}

		void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{

			switch (e.Action)
			{
				case NotifyCollectionChangedAction.Add:

					var newList = (from i in e.NewItems.Cast<T>() select new ItemStatus(i, m_FilterCriteria(i))).ToList();
					m_KnownItems.InsertRange(e.NewStartingIndex, newList);

					break;

				case NotifyCollectionChangedAction.Remove:

					m_KnownItems.RemoveRange(e.OldStartingIndex, e.OldItems.Count);
					break;

				case NotifyCollectionChangedAction.Reset:

					m_KnownItems.Clear();

					foreach (var item in m_Source)
						m_KnownItems.Add(new ItemStatus(item, m_FilterCriteria(item)));

					break;

				case NotifyCollectionChangedAction.Replace:

					var newList2 = (from i in e.NewItems.Cast<T>() select new ItemStatus(i, m_FilterCriteria(i))).ToList();
					for (var i = 0; i < newList2.Count; i++)
					{
						m_KnownItems[e.NewStartingIndex + i] = newList2[i];
					}

					break;

				default:
					//TODO - Handle the other cases for e.Action
					throw new NotImplementedException(string.Format(CultureInfo.InvariantCulture, "Action type {0} is not supported.", e.Action));
			}

			UpdateVisibleList();

		}

		void UpdateVisibleList()
		{
			m_VisibleItems.Clear();
			m_VisibleItems.AddRange(m_KnownItems.Where(x => x.IsVisible).Take(m_MaxItems ?? int.MaxValue).Select(x => x.Item));

			if (m_MinItems.HasValue)
				while (m_VisibleItems.Count < m_MinItems)
					m_VisibleItems.Add(m_PaddingFunction());

			OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

		}

		void OnItemPropertyChanged(object sender, RelayedEventArgs<PropertyChangedEventArgs> e)
		{
			var visibleChanged = false;
			foreach (var item in m_KnownItems)
			{
				if (Object.Equals(item.Item, e.OriginalSender))
				{
					var newVisible = m_FilterCriteria(item.Item);
					if (newVisible != item.IsVisible)
					{
						visibleChanged = true;
						item.IsVisible = newVisible;
					}
					break;
				}
			}
			if (visibleChanged)
				UpdateVisibleList();
		}

		class ItemStatus
		{
			readonly T m_Item;
			public ItemStatus(T item, bool isVisible)
			{
				m_Item = item;
				IsVisible = isVisible;
			}

			public bool IsVisible { get; set; }

			public T Item
			{
				get { return m_Item; }
			}

		}

		/// <summary>
		/// Occurs when the collection changes.
		/// </summary>
		public event NotifyCollectionChangedEventHandler CollectionChanged;


		/// <summary>
		/// Triggers the CollectionChanged event.
		/// </summary>
		void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
		{
			if (CollectionChanged != null)
				CollectionChanged(this, e);
		}


		void ICollection<T>.Add(T item)
		{
			throw new InvalidOperationException("Collection is read-only");
		}

		void ICollection<T>.Clear()
		{
			throw new InvalidOperationException("Collection is read-only");
		}

		/// <summary>
		/// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1" /> contains a specific value.
		/// </summary>
		/// <returns>
		/// true if <paramref name="item" /> is found in the <see cref="T:System.Collections.Generic.ICollection`1" />; otherwise, false.
		/// </returns>
		/// <param name="item">
		/// The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1" />.
		/// </param>
		public bool Contains(T item)
		{
			return m_VisibleItems.Contains(item);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="array"></param>
		/// <param name="arrayIndex"></param>
		public void CopyTo(T[] array, int arrayIndex)
		{
			m_VisibleItems.CopyTo(array, arrayIndex);
		}

		/// <summary>
		/// Gets the number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1" />.
		/// </summary>
		/// <returns>
		/// The number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1" />.
		/// </returns>
		public int Count
		{
			get { return m_VisibleItems.Count; }
		}

		/// <summary>
		/// Gets a value indicating whether the <see cref="T:System.Collections.Generic.ICollection`1" /> is read-only.
		/// </summary>
		/// <returns>
		/// true if the <see cref="T:System.Collections.Generic.ICollection`1" /> is read-only; otherwise, false.
		/// </returns>
		public bool IsReadOnly
		{
			get { return true; }
		}

		bool ICollection<T>.Remove(T item)
		{
			throw new InvalidOperationException("Collection is read-only");
		}

		/// <summary>
		/// Returns an enumerator that iterates through the collection.
		/// </summary>
		/// <returns>
		/// A <see cref="T:System.Collections.Generic.IEnumerator`1" /> that can be used to iterate through the collection.
		/// </returns>
		/// <filterpriority>1</filterpriority>
		public IEnumerator<T> GetEnumerator()
		{
			return m_VisibleItems.GetEnumerator();
		}

		IEnumerator IEnumerable.GetEnumerator()
		{
			return m_VisibleItems.GetEnumerator();
		}

		public int? MaxItems
		{
			get { return m_MaxItems; }
		}

		public int? MinItems
		{
			get { return m_MinItems; }
		}

	}
}
